#!/usr/bin/env ruby

require 'base64'
require 'digest'
require 'openssl'
require 'shellwords'

prefix = "ruby_#{RUBY_VERSION}-rails_#{ENV.fetch 'RAILS'}-"
BUNDLE = prefix + 'bundle'
APP    = prefix + 'app'

def download_bundle
  s3 :save, file: "#{BUNDLE}.sha2", as: "remote-#{BUNDLE}.sha2"
  s3 :save, file: "#{BUNDLE}.tgz",  as: "remote-#{BUNDLE}.tgz", untar: true
end

def download_app
  # Force test app re-build if dependencies were updated
  unless digest_changed? 'Gemfile.lock',   "remote-#{BUNDLE}.sha2", "#{BUNDLE}.sha2"
    s3 :save,   file: "#{APP}.sha2",   as: "remote-#{APP}.sha2"
    # Force test app re-build if spec/support changed
    unless digest_changed? 'spec/support', "remote-#{APP}.sha2",    "#{APP}.sha2"
      s3 :save, file: "#{APP}.tgz",    as: "remote-#{APP}.tgz", untar: true
    end
  end
end

def upload
  unless ID && SECRET
    puts "S3 credentials missing"
    return
  end

  [ ['Gemfile.lock', BUNDLE, 'bundle'],
    ['spec/support', APP,    'spec/rails']
  ].each do |to_check, name, to_save|
    puts "=> Checking #{to_check} for changes"
    if ret = digest_changed?(to_check, "remote-#{name}.sha2", "#{name}.sha2")
      puts "  => Changes found: #{ret[:old]} -> #{ret[:new]}"

      puts "  => Creating an archive"
      `tar -cjf #{name}.tgz #{to_save}`

      puts "  => Uploading a new archive"
      s3 :upload, file: "#{name}.tgz"
      s3 :upload, file: "#{name}.sha2"
    else
      puts "  => There were no changes, doing nothing"
    end
  end
end

def digest_changed?(to_check, old_digest_file, new_digest_file)
  if File.exists? new_digest_file
    digest = File.read new_digest_file
  else
    # Supports a single file, as well as a folder
    files   = Dir[to_check, "#{to_check}/**/*"].reject{ |f| File.directory? f }
    content = files.sort!.map{ |f| File.read f }.join
    digest  = Digest::SHA2.hexdigest content
    File.write new_digest_file, digest
  end

  old_digest = File.read old_digest_file if File.exists? old_digest_file

  {old: old_digest, new: digest} if digest != old_digest
end

def sign(secret, to_sign)
  Base64.strict_encode64 OpenSSL::HMAC.digest OpenSSL::Digest::SHA1.new, secret, to_sign
end

ID     = ENV['AWS_S3_ID']
SECRET = ENV['AWS_S3_SECRET']
URL    = 'https://s3.amazonaws.com/ActiveAdmin'

# s3 :list
# s3 :upload, file: 'foo'
# s3 :find,   file: 'foo'
# s3 :save,   file: 'foo', as: 'bar'
# s3 :save,   file: 'foo', untar: true
def s3(action, options = {})
  verb      = {list: :get, upload: :put, find: :get, save: :get}.fetch action
  file      = options.fetch(:file) unless action == :list
  extra_arg =  case action
  when :upload then "-T #{file}"
  when :save   then options[:as] ? "-o #{options[:as]}" : '-O'
  end
  do_after  = "&& tar -xf #{options[:as] || file}" if options[:untar]

  if ID && SECRET
    now       = Time.now.strftime "%a, %d %b %Y %H:%M:%S %z"
    signature = sign SECRET, "#{verb.upcase}\n\n\n#{now}\n/ActiveAdmin/#{file}"
    headers   = ["Authorization: AWS #{ID}:#{signature}", "Date: #{now}"].map do |h|
      "-H #{Shellwords.escape h}"
    end.join ' '
  end

  output = `curl -f #{headers} #{extra_arg} #{URL}/#{file} #{do_after}`
  [$?.success?, output]
end

if %w[download_bundle download_app upload].include? ARGV[0]
  send ARGV[0]
else
  raise "unexpected argument(s): #{ARGV}"
end
